/*
 *	GoogleSynchronizor is used to coordinate the synchronization processes between 
 *	local storage of BeautyRoxy system and the online Google Calendar cloud. 
 *	
 *	Generally, a synchronization process is divided into two parts. One is to sync 
 *	from local to Google, while the other is to sync from Google to local. 
 *	
 *	Floating tasks are not sync, and deadline tasks are sync as time tasks but with 
 *	a prefix DEADLINE_TASK_PREFIX before the task names.
 *	
 *	The default calendar that BeautyRoxy is sync to is the primary calendar of the 
 *	corresponding Google account. 
 *	
 */
// @author A0105514U
/** @author A0105514U */

package gCalEngine;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.TimeZone;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.api.client.util.DateTime;
import com.google.api.services.calendar.Calendar;
import com.google.api.services.calendar.model.Event;
import com.google.api.services.calendar.model.EventDateTime;

import storage.ModificationRecord;
import storage.Storage;
import global.Constant;
import global.InternetConnectionDetector;
import global.Task;
import global.Time;

public class GoogleSynchronizor {
	private final String CANCELLED = "cancelled";
	private final String UTC_ZONE = "UTC";
	private final String DEADLINE_TASK_PREFIX = "deadline: ";
	private final int DEADLINE_TASK_PREFIX_LENGTH = DEADLINE_TASK_PREFIX.length();
	private final int YEAR_STRING_START = 0;
	private final int YEAR_STRING_LENGTH = 4;
	private final int MONTH_STRING_START = 5;
	private final int MONTH_STRING_LENGTH = 2;
	private final int DATE_STRING_START = 8;
	private final int DATE_STRING_LENGTH = 2;
	private final int HOUR_STRING_START = 11;
	private final int HOUR_STRING_LENGTH = 2;
	private final int MIN_STRING_START = 14;
	private final int MIN_STRING_LENGTH = 2;
	private final int ZERO = 0;
	private final int EMPTY_STRING_LENGTH = 0;
	
	private ModificationRecord record;
	private String fileName;
	private String deletedTaskId;
	private GoogleSyncRecord syncRecord;
	
	private Logger logger;
	private FileHandler fh;
	private final String SYNCHRONIZOR_LOG_FILE = "synchronizorLog.txt";
			
	public GoogleSynchronizor(){
		logger = Logger.getLogger(GoogleSynchronizor.class.getName());
		try{
			fh = new FileHandler(SYNCHRONIZOR_LOG_FILE);
			logger.addHandler(fh);
		}catch(IOException e){
			
		}
		
		logger.log(Level.INFO,"Constructing Google Synchronizor...");
		record = null;
	}
	
	// Entry point of the sync functionality
	public void sync(Calendar service) throws IOException{
		InternetConnectionDetector detector = InternetConnectionDetector.getDetector();
		if(detector.detect()){
			logger.log(Level.INFO,"Syncing");
			logger.log(Level.INFO,"Starting Google To Local Sync...");
			GoogleToLocalSync(service);
			logger.log(Level.INFO,"Google To Local Sync finished");
			logger.log(Level.INFO,"Starting Local To Google Sync...");
			LocalToGoogleSync(service);
			logger.log(Level.INFO,"Local To Google Sync finished...");
		}
	}
	
	/* Sync task information from google to local.
	 * 
	 */
	private void GoogleToLocalSync(Calendar service) throws IOException{
		logger.log(Level.INFO,"Starting syncExistingFilesOnGoogle()");
		syncExistingFilesOnGoogle(service);
		logger.log(Level.INFO,"Finishing syncExistingFilesOnGoogle()");
		
		logger.log(Level.INFO,"Starting syncUnsyncTasks()");
		syncUnsyncTasks();
		logger.log(Level.INFO,"Finishing syncUnsyncTasks()");
		
		logger.log(Level.INFO,"Starting deleteInexistingFilesOnGoogle()");
		deleteInexistingFilesOnGoogle();
		logger.log(Level.INFO,"Finishing deleteInexistingFilesOnGoogle()");
	}
	
	private void syncExistingFilesOnGoogle(Calendar service) throws IOException{
		String pageToken = null;
		com.google.api.services.calendar.model.Events events;
		Task task;
		String eventStatus;

		syncRecord = constructRecord();
		
		do{
			events = service.events().list(Constant.DEFAULT_CALENDAR_ID).setPageToken(pageToken).setShowDeleted(true).execute();
			java.util.List<Event> items = events.getItems();
			
			for(Event e: items){
				task = eventToTask(e);
				eventStatus = e.getStatus();
				
				if(eventStatus.equalsIgnoreCase(CANCELLED)){
					// delete files that have been cancelled on Google Calendar
					deleteCancelledFile(task,e);
				}else{
					// add other events to local storage
					addActiveEventToLocal(task);
				}
			}
			
			pageToken = events.getNextPageToken();
		}while(pageToken != null);
	}
	
	private void addActiveEventToLocal(Task task) throws IOException{
		if(task != null){
			logger.log(Level.INFO,"Adding active task "+task.toString());

			String taskFileName = task.getTaskFileName();
			
			// If the file already exists, update task with the original done status
			Task origTask = Storage.getFileContentAsTask(taskFileName);
			if(origTask != null){
				initTaskAttribute(origTask,task);
			}
			syncRecord.setSync(taskFileName);
			Storage.addFileWithoutRecord(task);
			logger.log(Level.INFO,"Adding active task "+task.toString()+" successful.");

		}
	}
	
	private void initTaskAttribute(Task orig, Task newTask){
		if(orig.getTaskStatus()){
			newTask.markDone();
		}
	}
	
	private void deleteCancelledFile(Task task,Event e){
		if(task != null && chekcTaskIdIdentity(task,e)){
			logger.log(Level.INFO,"Deleting cancelled task "+task.toString());
			Storage.deleteFileWithoutRecord(task.getTaskFileName());
			logger.log(Level.INFO,"Deleting cancelled task "+task.toString()+" successful.");
		}
	}

	private boolean chekcTaskIdIdentity(Task task,Event e) {
		String taskId = Storage.getTaskIdFromFileName(task.getTaskFileName());
		String eventId = e.getId();
		return taskId.equalsIgnoreCase(eventId);
	}
	
	/*
	 * Delete files that are not existing on Google calendar cloud if any.
	 * 
	 */
	private void deleteInexistingFilesOnGoogle() {
		if(Storage.isRecordEmpty()){
			ArrayList<String> fileNamesNotOnGoogle = syncRecord.getUnsyncFileNameAsList();
			Iterator<String> it = fileNamesNotOnGoogle.iterator();
			String fileName;
			
			while(it.hasNext()){
				fileName = it.next();
				if(!Storage.isRecordExist(Constant.ADD_COMMAND_TYPE,fileName) && Storage.isFileSync(fileName)){
					logger.log(Level.INFO,"Deleting inexisting file on Google "+fileName);
					Storage.deleteFileWithoutRecord(fileName);
					logger.log(Level.INFO,"Deleting inexisting file on Google "+fileName+" successful");
				}
			}
		}
	}
	
	/*
	 * Check for files that are on local storage but have not been sync yet
	 * and then sync those task files
	 */
	private void syncUnsyncTasks() throws IOException{
		ArrayList<String> unsyncFileName = syncRecord.getUnsyncFileNameAsList();
		Iterator<String> it = unsyncFileName.iterator();
		String fileName;
		Task task;
		
		while(it.hasNext()){
			fileName = it.next();
			
			if(!Storage.isRecordExist(Constant.ADD_COMMAND_TYPE, fileName) && !Storage.isFileSync(fileName)){
				logger.log(Level.INFO,"Syncing unsync files "+fileName);
				task = Storage.getFileContentAsTask(fileName);
				Storage.addFile(task);
				logger.log(Level.INFO,"Successfully sync unsync files "+fileName);
			}
		}
	}

	private GoogleSyncRecord constructRecord() {
		ArrayList<String> fileNameList = Storage.getFileNameList();
		return new GoogleSyncRecord(fileNameList);
	}
	
	/*
	 * sync task information from local to Google.
	 * This mechanism is faciliated by the modRecord in Storage, which keeps track of all the unsync commands.
	 * LocalToGoogleSync is to read them and perform corresponding actions on Google Calendar 
	 */
	private void LocalToGoogleSync(Calendar service) throws IOException{
		if(Storage.isRecordEmpty()){
			return ;
		}else{
			record = Storage.getRecord();
			logger.log(Level.INFO,"Syncing from Local to Google: "+record.toString());
			if(record != null){
				if(isAddCommand(record)){
					fileName = record.getAddedFileName();
					assert(fileName.length() != EMPTY_STRING_LENGTH);
					Task addedTask = Storage.getFileContentAsTask(fileName);
					
					if(addedTask != null){
						// updatedTask will be null if the task added is a floating task
						Task updatedTask = addTaskToGoogleCalendar(addedTask,service);
						
						if(updatedTask == null){
							return ;
						}else{
							logger.log(Level.INFO,"Adding "+updatedTask.toString());
							updateLocalTaskWithSyncInfo(addedTask, updatedTask);
						}
					}
				}else if(isDeleteCommand(record)){
					deletedTaskId = record.getDeletedTaskId();
					Event event = service.events().get(Constant.DEFAULT_CALENDAR_ID, deletedTaskId).execute();
					Task task = eventToTask(event);
					if(task!=null){
						if(deletedTaskId.length()!=EMPTY_STRING_LENGTH && !event.getStatus().equalsIgnoreCase(CANCELLED)){
							service.events().delete(Constant.DEFAULT_CALENDAR_ID, deletedTaskId).execute();
							logger.log(Level.INFO,"Deleting "+task.getTaskName());
							Storage.deleteFileWithoutRecord(task.getTaskFileName());
						}
					}
				}else{
					logger.log(Level.SEVERE,"Unbound command type");
					return ;
				}
			}else{
				logger.log(Level.SEVERE, "Null record is returned.");
			}
		}
	}
	
	
	private Task addTaskToGoogleCalendar(Task addedTask, Calendar service) throws IOException{
		if(addedTask.getTaskType().equalsIgnoreCase(Constant.FLOAT_TASK_TYPE)){
			// Floating tasks are not sync to Google Calendar
			return null;
		}else if(addedTask.getTaskType().equalsIgnoreCase(Constant.DEADLINE_TASK_TYPE)){
			// Deadline tasks are sync to Google Calendar as a time tasks, but with a prefix "deadline: " before task names
			Time end = addedTask.getEndTime();
			Time start  = new Time(end.getYear(),end.getMonth()+1,end.getDate(),end.getHours()-1,end.getMinutes());
			addedTask.setStartTime(start);
			addedTask.setTaskName(DEADLINE_TASK_PREFIX+addedTask.getTaskName());
		}

		Event createdEvent = createGCalEvent(addedTask);
		Event addedEvent = addGCalEvent(service,createdEvent);
		Task updatedTask = updateTask(addedTask,addedEvent);
		if(addedTask.getTaskStatus()){
			updatedTask.markDone();
		}
		return updatedTask;
	}
	
	// basically replace the original unsync task files in local storage by updated tasks that are generated from Google
	// Calendar events which have taskId with them and whose taskSyncStatus is set to be true
	private void updateLocalTaskWithSyncInfo(Task addedTask, Task updatedTask) throws IOException{
		String taskName = addedTask.getTaskName();
		
		// Remove DEADLINE_TASK_PREFIX while pulling tasks from Google Calendar, if any
		if(taskName.length() >DEADLINE_TASK_PREFIX_LENGTH && 
			taskName.substring(ZERO,DEADLINE_TASK_PREFIX_LENGTH).equalsIgnoreCase(DEADLINE_TASK_PREFIX)){
			
			taskName = taskName.substring(DEADLINE_TASK_PREFIX_LENGTH);
		}
		
		addedTask.setTaskName(taskName);
		replaceTask(addedTask,updatedTask);
	}
	
	private Task eventToTask(Event e){
		String defaultTaskType = Constant.TIME_TASK_TYPE;
		String taskName = e.getSummary();
		
		if(taskName == null){
			return null;
		}
		
		if(taskName.length() > DEADLINE_TASK_PREFIX_LENGTH && taskName.substring(ZERO,DEADLINE_TASK_PREFIX_LENGTH).equalsIgnoreCase(DEADLINE_TASK_PREFIX)){
			defaultTaskType = Constant.DEADLINE_TASK_TYPE;
			taskName = taskName.substring(DEADLINE_TASK_PREFIX_LENGTH);
		}
		
		String taskLocation = e.getLocation();
		
		EventDateTime taskStartDateTime = e.getStart();
		Time startTime = eventDateTimeToTime(taskStartDateTime);

		EventDateTime taskEndDateTime = e.getEnd();
		Time endTime = eventDateTimeToTime(taskEndDateTime);
		
		Task task = new Task(defaultTaskType,taskName,taskLocation,startTime,endTime);
		task.setSync();
		task.setId(e.getId());
		
		return task;
	}
	
	private Time eventDateTimeToTime(EventDateTime dateTime){
		DateTime date = dateTime.getDateTime();
		if(date == null){
			return null;
		}else{
			String timeString = getTimeString(date);
			Time time = new Time(timeString);
			return time;
		}
	}
	
	private String getTimeString(DateTime date){
		String dateTimeString = date.toString();
		String yearString = dateTimeString.substring(YEAR_STRING_START,ZERO + YEAR_STRING_LENGTH);
		String monthString = dateTimeString.substring(MONTH_STRING_START,MONTH_STRING_START + MONTH_STRING_LENGTH);
		String dateString = dateTimeString.substring(DATE_STRING_START,DATE_STRING_START+DATE_STRING_LENGTH);
		String hourString = dateTimeString.substring(HOUR_STRING_START,HOUR_STRING_START+HOUR_STRING_LENGTH);
		String minString = dateTimeString.substring(MIN_STRING_START,MIN_STRING_START+MIN_STRING_LENGTH);
		return yearString+monthString+dateString+hourString+minString;
	}
	
	private void replaceTask(Task addedTask, Task updatedTask) throws IOException{
		Storage.deleteFileWithoutRecord(addedTask.getTaskFileName());
		Storage.addFileWithoutRecord(updatedTask);
	}
	
	private Event addGCalEvent(Calendar service,Event createdEvent) throws IOException{
		return service.events().insert(Constant.DEFAULT_CALENDAR_ID,createdEvent).execute();
	}
	
	private Task updateTask(Task addedTask, Event addedEvent){
		Task updatedTask = addedTask;
		updatedTask.setId(addedEvent.getId());
		updatedTask.setSync();
		if(addedTask.getTaskStatus()){
			updatedTask.markDone();
		}
		return updatedTask;
	}
	
	private boolean isAddCommand(ModificationRecord record){
		String type = record.getModType();
		return type.equalsIgnoreCase(Constant.ADD_COMMAND_TYPE);
	}
	
	private boolean isDeleteCommand(ModificationRecord record){
		String type = record.getModType();
		return type.equalsIgnoreCase(Constant.DELETE_COMMAND_TYPE);		
	}
	
	// create Google Calendar event from Task
	private Event createGCalEvent(Task task){
		Event event = new Event();
		event.setSummary(task.getTaskName());
		event.setLocation(task.getTaskLocation());
		Date startTime = Time.timeToDate(task.getStartTime());
		Date endTime = Time.timeToDate(task.getEndTime());
		DateTime start = new DateTime(startTime,TimeZone.getTimeZone(UTC_ZONE));
		DateTime end = new DateTime(endTime,TimeZone.getTimeZone(UTC_ZONE));
		event.setStart(new EventDateTime().setDateTime(start));
		event.setEnd(new EventDateTime().setDateTime(end));
		return event;
	}
}